//
// The MIT License (MIT)
//
// Copyright (c) 2016 Alexander Kormanovsky
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

#include "ios.h"

#if TARGET_OS_IPHONE

#include <Foundation/Foundation.h>

#include <fstream>
#include <zlib.h>
#include <sys/stat.h>

#ifndef TAR_DEBUG
#  define TAR_DEBUG 0
#endif

#define INTERNAL_DIR        "Library"
#define TZDATA_DIR          "tzdata"
#define TARGZ_EXTENSION     "tar.gz"

#define TAR_BLOCK_SIZE                  512
#define TAR_TYPE_POSITION               156
#define TAR_NAME_POSITION               0
#define TAR_NAME_SIZE                   100
#define TAR_SIZE_POSITION               124
#define TAR_SIZE_SIZE                   12

namespace arrow_vendored
{
namespace date
{
namespace iOSUtils
{
    
struct TarInfo
{
    char objType;
    std::string objName;
    size_t realContentSize; // writable size without padding zeroes
    size_t blocksContentSize; // adjusted size to 512 bytes blocks
    bool success;
};

std::string convertCFStringRefPathToCStringPath(CFStringRef ref);
bool extractTzdata(CFURLRef homeUrl, CFURLRef archiveUrl, std::string destPath);
TarInfo getTarObjectInfo(std::ifstream &readStream);
std::string getTarObject(std::ifstream &readStream, int64_t size);
bool writeFile(const std::string &tzdataPath, const std::string &fileName,
            const std::string &data, size_t realContentSize);

std::string
get_current_timezone()
{
    CFTimeZoneRef tzRef = CFTimeZoneCopySystem();
    CFStringRef tzNameRef = CFTimeZoneGetName(tzRef);
    CFIndex bufferSize = CFStringGetLength(tzNameRef) + 1;
    char buffer[bufferSize];
    
    if (CFStringGetCString(tzNameRef, buffer, bufferSize, kCFStringEncodingUTF8))
    {
        CFRelease(tzRef);
        return std::string(buffer);
    }
    
    CFRelease(tzRef);
    
    return "";
}

std::string
get_tzdata_path()
{
    CFURLRef homeUrlRef = CFCopyHomeDirectoryURL();
    CFStringRef homePath = CFURLCopyPath(homeUrlRef);
    std::string path(std::string(convertCFStringRefPathToCStringPath(homePath)) +
                    INTERNAL_DIR + "/" + TZDATA_DIR);
    std::string result_path(std::string(convertCFStringRefPathToCStringPath(homePath)) +
                            INTERNAL_DIR);
    
    if (access(path.c_str(), F_OK) == 0)
    {
#if TAR_DEBUG
        printf("tzdata dir exists\n");
#endif
        CFRelease(homeUrlRef);
        CFRelease(homePath);
        
        return result_path;
    }
    
    CFBundleRef mainBundle = CFBundleGetMainBundle();
    CFArrayRef paths = CFBundleCopyResourceURLsOfType(mainBundle, CFSTR(TARGZ_EXTENSION),
                                                    NULL);
    
    if (CFArrayGetCount(paths) != 0)
    {
        // get archive path, assume there is no other tar.gz in bundle
        CFURLRef archiveUrl = static_cast<CFURLRef>(CFArrayGetValueAtIndex(paths, 0));
        CFStringRef archiveName = CFURLCopyPath(archiveUrl);
        archiveUrl = CFBundleCopyResourceURL(mainBundle, archiveName, NULL, NULL);
        
        extractTzdata(homeUrlRef, archiveUrl, path);
        
        CFRelease(archiveUrl);
        CFRelease(archiveName);
    }
    
    CFRelease(homeUrlRef);
    CFRelease(homePath);
    CFRelease(paths);
    
    return result_path;
}

std::string
convertCFStringRefPathToCStringPath(CFStringRef ref)
{
    CFIndex bufferSize = CFStringGetMaximumSizeOfFileSystemRepresentation(ref);
    char *buffer = new char[bufferSize];
    CFStringGetFileSystemRepresentation(ref, buffer, bufferSize);
    auto result = std::string(buffer);
    delete[] buffer;
    return result;
}

bool
extractTzdata(CFURLRef homeUrl, CFURLRef archiveUrl, std::string destPath)
{
    std::string TAR_TMP_PATH = "/tmp.tar";
    
    CFStringRef homeStringRef = CFURLCopyPath(homeUrl);
    auto homePath = convertCFStringRefPathToCStringPath(homeStringRef);
    CFRelease(homeStringRef);
    
    CFStringRef archiveStringRef = CFURLCopyPath(archiveUrl);
    auto archivePath = convertCFStringRefPathToCStringPath(archiveStringRef);
    CFRelease(archiveStringRef);
    
    // create Library path
    auto libraryPath = homePath + INTERNAL_DIR;
    
    // create tzdata path
    auto tzdataPath = libraryPath + "/" + TZDATA_DIR;
    
    // -- replace %20 with " "
    const std::string search = "%20";
    const std::string replacement = " ";
    size_t pos = 0;
    
    while ((pos = archivePath.find(search, pos)) != std::string::npos) {
        archivePath.replace(pos, search.length(), replacement);
        pos += replacement.length();
    }
    
    gzFile tarFile = gzopen(archivePath.c_str(), "rb");
    
    // create tar unpacking path
    auto tarPath = libraryPath + TAR_TMP_PATH;
    
    // create tzdata directory
    mkdir(destPath.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
    
    // ======= extract tar ========
    
    std::ofstream os(tarPath.c_str(), std::ofstream::out | std::ofstream::app);
    unsigned int bufferLength = 1024 * 256;  // 256Kb
    unsigned char *buffer = (unsigned char *)malloc(bufferLength);
    bool success = true;
    
    while (true)
    {
        int readBytes = gzread(tarFile, buffer, bufferLength);
        
        if (readBytes > 0)
        {
            os.write((char *) &buffer[0], readBytes);
        }
        else
            if (readBytes == 0)
            {
                break;
            }
            else
                if (readBytes == -1)
                {
                    printf("decompression failed\n");
                    success = false;
                    break;
                }
                else
                {
                    printf("unexpected zlib state\n");
                    success = false;
                    break;
                }
    }
    
    os.close();
    free(buffer);
    gzclose(tarFile);
    
    if (!success)
    {
        remove(tarPath.c_str());
        return false;
    }
    
    // ======== extract files =========
    
    uint64_t location = 0; // Position in the file
    
    // get file size
    struct stat stat_buf;
    int res = stat(tarPath.c_str(), &stat_buf);
    if (res != 0)
    {
        printf("error file size\n");
        remove(tarPath.c_str());
        return false;
    }
    int64_t tarSize = stat_buf.st_size;
    
    // create read stream
    std::ifstream is(tarPath.c_str(), std::ifstream::in | std::ifstream::binary);
    
    // process files
    while (location < tarSize)
    {
        TarInfo info = getTarObjectInfo(is);
        
        if (!info.success || info.realContentSize == 0)
        {
            break; // something wrong or all files are read
        }
        
        switch (info.objType)
        {
            case '0':   // file
            case '\0':  //
            {
                std::string obj = getTarObject(is, info.blocksContentSize);
#if TAR_DEBUG
                size += info.realContentSize;
                printf("#%i %s file size %lld written total %ld from %lld\n", ++count,
                    info.objName.c_str(), info.realContentSize, size, tarSize);
#endif
                writeFile(tzdataPath, info.objName, obj, info.realContentSize);
                location += info.blocksContentSize;
                
                break;
            }
        }
    }
    
    remove(tarPath.c_str());
    
    return true;
}

TarInfo
getTarObjectInfo(std::ifstream &readStream)
{
    int64_t length = TAR_BLOCK_SIZE;
    char buffer[length];
    char type;
    char name[TAR_NAME_SIZE + 1];
    char sizeBuf[TAR_SIZE_SIZE + 1];
    
    readStream.read(buffer, length);
    
    memcpy(&type, &buffer[TAR_TYPE_POSITION], 1);
    
    memset(&name, '\0', TAR_NAME_SIZE + 1);
    memcpy(&name, &buffer[TAR_NAME_POSITION], TAR_NAME_SIZE);
    
    memset(&sizeBuf, '\0', TAR_SIZE_SIZE + 1);
    memcpy(&sizeBuf, &buffer[TAR_SIZE_POSITION], TAR_SIZE_SIZE);
    size_t realSize = strtol(sizeBuf, NULL, 8);
    size_t blocksSize = realSize + (TAR_BLOCK_SIZE - (realSize % TAR_BLOCK_SIZE));
    
    return {type, std::string(name), realSize, blocksSize, true};
}

std::string
getTarObject(std::ifstream &readStream, int64_t size)
{
    char buffer[size];
    readStream.read(buffer, size);
    return std::string(buffer);
}

bool
writeFile(const std::string &tzdataPath, const std::string &fileName, const std::string &data,
        size_t realContentSize)
{
    std::ofstream os(tzdataPath + "/" + fileName, std::ofstream::out | std::ofstream::binary);
    
    if (!os) {
        return false;
    }
    
    // trim empty space
    char trimmedData[realContentSize + 1];
    memset(&trimmedData, '\0', realContentSize);
    memcpy(&trimmedData, data.c_str(), realContentSize);
    
    // write
    os.write(trimmedData, realContentSize);
    os.close();
    
    return true;
}
    
}  // namespace iOSUtils
}  // namespace date
}  // namespace arrow_vendored

#endif  // TARGET_OS_IPHONE
